#ifndef QEXPANDOPENGLWIDGET_H
#define QEXPANDOPENGLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLExtraFunctions>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/ext.hpp>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <QDebug>
#include <QMouseEvent>
#include <QPoint>
#include <vector>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <stb_image.h>

#include <SOIL/SOIL.h>


#define MAX_BONE_INFLUENCE 4

class QExpandOpenGLWidget: public QOpenGLWidget, protected QOpenGLExtraFunctions
{
public:
    class Shader;
    struct Vertex;
    struct Texture;
    class Mesh;
    class Model;
    class Texture2D;
    class ResourceManager;
    class SpriteRenderer;
    class Game;
    enum GameState;
    class GameObject;
    class GameLevel;
public:
    QExpandOpenGLWidget(){}

    void resizeGL(int w, int h);

    static unsigned int TextureFromFile(const char *path, const std::string &directory, QExpandOpenGLWidget* m_parent);

    unsigned int loadTexture(const char *path);

    // 加载立方体纹理
    unsigned int loadCubemap(std::vector<std::string> faces);
};

class QExpandOpenGLWidget::Shader {
public:
    unsigned int shaderProgram;
    QExpandOpenGLWidget* m_parent = nullptr;
    Shader(){}
    Shader(QExpandOpenGLWidget* parent): m_parent(parent) {}
    Shader(const Shader& shader) {
        this->shaderProgram = shader.shaderProgram;
        this->m_parent = shader.m_parent;
    }
    // 读取shader配置文件
    void readShaderFile(const char* vertexPath, const char* fragmentPath, const char* geometryPath = nullptr);

    // 读取shader配置字符串
    void readShaderStr(const char* vertexStr, const char* fragmentStr, const char* geometryStr = nullptr);

    // utility function for checking shader compilation/linking errors.
    // ------------------------------------------------------------------------
    void checkCompileErrors(GLuint shader, std::string type);

    // activate the shader
    // ------------------------------------------------------------------------
    void use();

    Shader &Use();
    void    SetFloat    (const GLchar *name, GLfloat value, GLboolean useShader = false);
    void    SetInteger  (const GLchar *name, GLint value, GLboolean useShader = false);
    void    SetVector2f (const GLchar *name, GLfloat x, GLfloat y, GLboolean useShader = false);
    void    SetVector2f (const GLchar *name, const glm::vec2 &value, GLboolean useShader = false);
    void    SetVector3f (const GLchar *name, GLfloat x, GLfloat y, GLfloat z, GLboolean useShader = false);
    void    SetVector3f (const GLchar *name, const glm::vec3 &value, GLboolean useShader = false);
    void    SetVector4f (const GLchar *name, GLfloat x, GLfloat y, GLfloat z, GLfloat w, GLboolean useShader = false);
    void    SetVector4f (const GLchar *name, const glm::vec4 &value, GLboolean useShader = false);
    void    SetMatrix4  (const GLchar *name, const glm::mat4 &matrix, GLboolean useShader = false);
    // utility uniform functions
    // ------------------------------------------------------------------------
    void setBool(const std::string &name, bool value);

    // ------------------------------------------------------------------------
    void setInt(const std::string &name, int value);

    // ------------------------------------------------------------------------
    void setFloat(const std::string &name, float value);

    // ------------------------------------------------------------------------
    void setVec2(const std::string &name, const glm::vec2 &value);

    void setVec2(const std::string &name, float x, float y);

    // ------------------------------------------------------------------------
    void setVec3(const std::string &name, const glm::vec3 &value);

    void setVec3(const std::string &name, float x, float y, float z);

    // ------------------------------------------------------------------------
    void setVec4(const std::string &name, const glm::vec4 &value);

    void setVec4(const std::string &name, float x, float y, float z, float w);

    // ------------------------------------------------------------------------
    void setMat2(const std::string &name, const glm::mat2 &mat);

    // ------------------------------------------------------------------------
    void setMat3(const std::string &name, const glm::mat3 &mat);

    // ------------------------------------------------------------------------
    void setMat4(const std::string &name, const glm::mat4 &mat);
};

struct QExpandOpenGLWidget::Vertex {
    // position
    glm::vec3 Position;
    // normal
    glm::vec3 Normal;
    // texCoords
    glm::vec2 TexCoords;
    // tangent
    glm::vec3 Tangent;
    // bitangent
    glm::vec3 Bitangent;
    //bone indexes which will influence this vertex
    int m_BoneIDs[MAX_BONE_INFLUENCE];
    //weights from each bone
    float m_Weights[MAX_BONE_INFLUENCE];
};

struct QExpandOpenGLWidget::Texture {
    unsigned int id;
    std::string type;
    std::string path;
};

class QExpandOpenGLWidget::Mesh {
public:
    // mesh Data
    std::vector<Vertex>       vertices;
    std::vector<unsigned int> indices;
    std::vector<Texture>      textures;
    unsigned int VAO;
    QExpandOpenGLWidget *m_parent;
    // constructor
    Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures, QExpandOpenGLWidget *parent);

    // render the mesh
    void Draw(Shader &shader);

private:
    // render data
    unsigned int VBO, EBO;

    // initializes all the buffer objects/arrays
    void setupMesh();

};

class QExpandOpenGLWidget::Model
{
public:
    // model data
    std::vector<Texture> textures_loaded;	// stores all the textures loaded so far, optimization to make sure textures aren't loaded more than once.
    std::vector<Mesh>    meshes;
    std::string directory;
    QExpandOpenGLWidget* m_parent;
    bool gammaCorrection;

    // constructor, expects a filepath to a 3D model.
    Model(std::string const &path, QExpandOpenGLWidget* parent, bool gamma = false) : gammaCorrection(gamma)
    {
        this->m_parent = parent;
        loadModel(path);
    }

    // draws the model, and thus all its meshes
    void Draw(Shader &shader);

private:
    // loads a model with supported ASSIMP extensions from file and stores the resulting meshes in the meshes vector.
    void loadModel(std::string const &path);

    // processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any).
    void processNode(aiNode *node, const aiScene *scene);

    Mesh processMesh(aiMesh *mesh, const aiScene *scene);

    // checks all material textures of a given type and loads the textures if they're not loaded yet.
    // the required info is returned as a Texture struct.
    std::vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, std::string typeName);
};

class QExpandOpenGLWidget::Texture2D{
private:
    QExpandOpenGLWidget* m_parent;
public:
    // Holds the ID of the texture object, used for all texture operations to reference to this particlar texture
    GLuint ID;
    // Texture image dimensions
    GLuint Width, Height; // Width and height of loaded image in pixels
    // Texture Format
    GLuint Internal_Format; // Format of texture object
    GLuint Image_Format; // Format of loaded image
    // Texture configuration
    GLuint Wrap_S; // Wrapping mode on S axis
    GLuint Wrap_T; // Wrapping mode on T axis
    GLuint Filter_Min; // Filtering mode if texture pixels < screen pixels
    GLuint Filter_Max; // Filtering mode if texture pixels > screen pixels
    // Constructor (sets default texture modes)
    Texture2D(QExpandOpenGLWidget* parent) :m_parent(parent), Width(0), Height(0), Internal_Format(GL_RGB), Image_Format(GL_RGB), Wrap_S(GL_REPEAT), Wrap_T(GL_REPEAT), Filter_Min(GL_LINEAR), Filter_Max(GL_LINEAR)
    {
        m_parent->glGenTextures(1, &this->ID);
    }
    Texture2D(): Width(0), Height(0), Internal_Format(GL_RGB), Image_Format(GL_RGB), Wrap_S(GL_REPEAT), Wrap_T(GL_REPEAT), Filter_Min(GL_LINEAR), Filter_Max(GL_LINEAR)
    {

    }
    // Generates texture from image data
    void Generate(GLuint width, GLuint height, unsigned char* data){
        this->Width = width;
        this->Height = height;
        // Create Texture
        m_parent->glBindTexture(GL_TEXTURE_2D, this->ID);
        // Set Texture wrap and filter modes
        m_parent->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, this->Wrap_S);
        m_parent->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, this->Wrap_T);
        m_parent->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, this->Filter_Min);
        m_parent->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, this->Filter_Max);
        m_parent->glTexImage2D(GL_TEXTURE_2D, 0, this->Internal_Format, width, height, 0, this->Image_Format, GL_UNSIGNED_BYTE, data);
        m_parent->glGenerateMipmap(GL_TEXTURE_2D);
        // Unbind texture
        m_parent->glBindTexture(GL_TEXTURE_2D, 0);
    }
    // Binds the texture as the current active GL_TEXTURE_2D texture object
    void Bind() const
    {
        m_parent->glBindTexture(GL_TEXTURE_2D, this->ID);
    }
};


class QExpandOpenGLWidget::ResourceManager
{
public:
    static QExpandOpenGLWidget *parent;
    // Resource storage
    static std::map<std::string, QExpandOpenGLWidget::Shader>    Shaders;
    static std::map<std::string, QExpandOpenGLWidget::Texture2D> Textures;
    // Loads (and generates) a shader program from file loading vertex, fragment (and geometry) shader's source code. If gShaderFile is not nullptr, it also loads a geometry shader
    static QExpandOpenGLWidget::Shader   LoadShader(const GLchar *vShaderFile, const GLchar *fShaderFile, const GLchar *gShaderFile, std::string name);
    // Retrieves a stored sader
    static QExpandOpenGLWidget::Shader   GetShader(std::string name);
    // Loads (and generates) a texture from file
    static QExpandOpenGLWidget::Texture2D LoadTexture(const GLchar *file, GLboolean alpha, std::string name);
    // Retrieves a stored texture
    static QExpandOpenGLWidget::Texture2D GetTexture(std::string name);
    // Properly de-allocates all loaded resources
    static void      Clear();
private:
    // Private constructor, that is we do not want any actual resource manager objects. Its members and functions should be publicly available (static).
    ResourceManager() { }
    // Loads and generates a shader from file
    static QExpandOpenGLWidget::Shader loadShaderFromFile(const GLchar *vShaderFile, const GLchar *fShaderFile, const GLchar *gShaderFile = nullptr);
    // Loads a single texture from file
    static QExpandOpenGLWidget::Texture2D loadTextureFromFile(const GLchar *file, GLboolean alpha);
};

class QExpandOpenGLWidget::SpriteRenderer
{
public:
    SpriteRenderer(const Shader &shader, QExpandOpenGLWidget *parent);
    ~SpriteRenderer();

    void DrawSprite(const QExpandOpenGLWidget::Texture2D &texture, glm::vec2 position, glm::vec2 size = glm::vec2(10, 10), GLfloat rotate = 0.0f, glm::vec3 color = glm::vec3(1.0f));
public:
    GLuint VBO, VAO, EBO, quadVAO;
private:
    Shader shader;
    QExpandOpenGLWidget* m_parent;
    void initRenderData();
};
// 代表了游戏的当前状态
enum QExpandOpenGLWidget::GameState {
    GAME_ACTIVE,
    GAME_MENU,
    GAME_WIN
};

class QExpandOpenGLWidget::Game
{
public:
    // 游戏状态
    GameState  State;
    GLboolean  Keys[1024];
    GLuint     Width, Height;
    std::vector<GameLevel> Levels;
    GLuint                 Level;
    // 构造函数/析构函数
    Game(GLuint width, GLuint height, QExpandOpenGLWidget* parent);
    ~Game();
    // 初始化游戏状态（加载所有的着色器/纹理/关卡）
    void Init();
    // 游戏循环
    void ProcessInput(GLfloat dt);
    void Update(GLfloat dt);
    void Render();
    QExpandOpenGLWidget::SpriteRenderer  *Renderer;
    QExpandOpenGLWidget::GameObject      *Player;
    QExpandOpenGLWidget* m_parent;
};

class QExpandOpenGLWidget::GameObject
{
public:
    // object state
    glm::vec2   Position, Size, Velocity;
    glm::vec3   Color;
    float       Rotation;
    bool        IsSolid;
    bool        Destroyed;
    // render state
    Texture2D   Sprite;
    // constructor(s)
    GameObject();
    GameObject(glm::vec2 pos, glm::vec2 size, Texture2D sprite, glm::vec3 color = glm::vec3(1.0f), glm::vec2 velocity = glm::vec2(0.0f, 0.0f));
    // draw sprite
    virtual void Draw(SpriteRenderer &renderer);
};

class QExpandOpenGLWidget::GameLevel
{
public:
    std::vector<GameObject> Bricks;

    GameLevel() { }
    // 从文件中加载关卡
    void Load(const GLchar *file, GLuint levelWidth, GLuint levelHeight);
    // 渲染关卡
    void Draw(SpriteRenderer &renderer);
    // 检查一个关卡是否已完成 (所有非坚硬的瓷砖均被摧毁)
    GLboolean IsCompleted();
private:
    // 由砖块数据初始化关卡
    void init(std::vector<std::vector<GLuint>> tileData, GLuint levelWidth, GLuint levelHeight);
};
#endif // QEXPANDOPENGLWIDGET_H
